package com.google.android.apps.skipbot;

import java.io.FileDescriptor;
import java.io.FileInputStream;
import java.io.FileOutputStream;
import java.io.IOException;

import roboguice.service.RoboService;
import android.app.Notification;
import android.app.NotificationManager;
import android.app.PendingIntent;
import android.content.BroadcastReceiver;
import android.content.Context;
import android.content.Intent;
import android.content.IntentFilter;
import android.os.Binder;
import android.os.Handler;
import android.os.IBinder;
import android.os.Message;
import android.os.ParcelFileDescriptor;
import android.util.Log;
import android.widget.Toast;

import com.android.future.usb.UsbAccessory;
import com.android.future.usb.UsbManager;

public class RobotService extends RoboService implements Runnable, RobotController {
  private static final String TAG = "Skipper";

  private static final String ACTION_USB_PERMISSION =
      "com.google.android.apps.skipbot.action.USB_PERMISSION";

  private UsbManager mUsbManager;
  private PendingIntent mPermissionIntent;
  private boolean mPermissionRequestPending;

  UsbAccessory mAccessory;
  ParcelFileDescriptor mFileDescriptor;
  FileInputStream mInputStream;
  FileOutputStream mOutputStream;
  Robot robot;

  public static final byte LED_SERVO_COMMAND = 2;

  private static final int NOTIFICATION_ID = 42;

  // Keeps track if we are enabled.
  private boolean enabled = false;

  private final BroadcastReceiver mUsbReceiver = new BroadcastReceiver() {
    @Override
    public void onReceive(Context context, Intent intent) {
      String action = intent.getAction();
      if (ACTION_USB_PERMISSION.equals(action)) {
        synchronized (this) {
          UsbAccessory accessory = UsbManager.getAccessory(intent);
          if (intent.getBooleanExtra(UsbManager.EXTRA_PERMISSION_GRANTED, false)) {
            openAccessory(accessory);
          } else {
            Log.d(TAG, "permission denied for accessory " + accessory);
          }
          mPermissionRequestPending = false;
        }
      } else if (UsbManager.ACTION_USB_ACCESSORY_DETACHED.equals(action)) {
        UsbAccessory accessory = UsbManager.getAccessory(intent);
        if (accessory != null && accessory.equals(mAccessory)) {
          closeAccessory();
        }
      }
    }
  };

  /** Called when the activity is first created. */
  @Override
  public void onCreate() {
    super.onCreate();

    mUsbManager = UsbManager.getInstance(this);
    mPermissionIntent = PendingIntent.getBroadcast(this, 0, new Intent(ACTION_USB_PERMISSION), 0);
    IntentFilter filter = new IntentFilter(ACTION_USB_PERMISSION);
    filter.addAction(UsbManager.ACTION_USB_ACCESSORY_DETACHED);
    registerReceiver(mUsbReceiver, filter);

    enableControls(false);
  }

  private void showNotification() {
    // In this sample, we'll use the same text for the ticker and the expanded
    // notification
    CharSequence text = getText(R.string.robot_service_started);

    // Set the icon, scrolling text and timestamp
    Notification notification =
        new Notification(android.R.drawable.stat_notify_more, text, System.currentTimeMillis());

    // The PendingIntent to launch our activity if the user selects this
    // notification
    PendingIntent contentIntent =
        PendingIntent.getActivity(this, 0, new Intent(this, SkipperPhone.class), 0);

    // Set the info for the views that show in the notification panel.
    notification.setLatestEventInfo(this, "Robot service is running", text, contentIntent);

    // Send the notification.
    NotificationManager notificationManager =
        (NotificationManager) getSystemService(NOTIFICATION_SERVICE);
    notificationManager.notify(NOTIFICATION_ID, notification);
  }

  @Override
  public int onStartCommand(Intent intent, int flags, int startId) {
    Log.i("RobotService", "Received start id " + startId + ": " + intent);
    // We want this service to continue running until it is explicitly
    // stopped, so return sticky.

    if (mInputStream != null && mOutputStream != null) {
      return START_STICKY;
    }

    UsbAccessory[] accessories = mUsbManager.getAccessoryList();
    UsbAccessory accessory = (accessories == null ? null : accessories[0]);
    if (accessory != null) {
      if (mUsbManager.hasPermission(accessory)) {
        openAccessory(accessory);
      } else {
        synchronized (mUsbReceiver) {
          if (!mPermissionRequestPending) {
            mUsbManager.requestPermission(accessory, mPermissionIntent);
            mPermissionRequestPending = true;
          }
        }
      }
    } else {
      Log.d(TAG, "mAccessory is null");
    }

    Toast.makeText(this, R.string.robot_service_started, Toast.LENGTH_SHORT).show();

    // Display a notification about us starting. We put an icon in the status
    // bar.
    showNotification();

    return START_STICKY;
  }

  @Override
  public void onDestroy() {
    closeAccessory();
    unregisterReceiver(mUsbReceiver);
    Toast.makeText(this, R.string.robot_service_stopped, Toast.LENGTH_SHORT).show();

    NotificationManager notificationManager =
        (NotificationManager) getSystemService(NOTIFICATION_SERVICE);
    notificationManager.cancel(NOTIFICATION_ID);
    super.onDestroy();
  }

  /**
   * Class for clients to access. Because we know this service always runs in
   * the same process as its clients, we don't need to deal with IPC.
   */
  public class LocalBinder extends Binder {
    RobotService getService() {
      return RobotService.this;
    }
  }

  private void openAccessory(UsbAccessory accessory) {
    mFileDescriptor = mUsbManager.openAccessory(accessory);
    if (mFileDescriptor != null) {
      mAccessory = accessory;
      FileDescriptor fd = mFileDescriptor.getFileDescriptor();
      mInputStream = new FileInputStream(fd);
      mOutputStream = new FileOutputStream(fd);
      Thread thread = new Thread(null, this, "Skipper");
      thread.start();
      Toast.makeText(this, "Connected", Toast.LENGTH_SHORT).show();
      Log.d(TAG, "accessory opened");
      enableControls(true);
    } else {
      Log.d(TAG, "accessory open fail");
    }
  }

  private void closeAccessory() {
    enableControls(false);

    try {
      if (mFileDescriptor != null) {
        mFileDescriptor.close();
      }
    } catch (IOException e) {
    } finally {
      mFileDescriptor = null;
      mAccessory = null;
    }
  }

  protected void enableControls(boolean enable) {
    enabled = enable;
    reloadRobot();
  }

  @Override
  public void run() {
    int ret = 0;
    byte[] buffer = new byte[16384];
    int i;

    while (ret >= 0) {
      try {
        ret = mInputStream.read(buffer);
      } catch (IOException e) {
        break;
      }

      i = 0;
      while (i < ret) {
        int len = ret - i;

        switch (buffer[i]) {
        // case 0x1:
        // if (len >= 3) {
        // Message m = Message.obtain(mHandler, MESSAGE_SWITCH);
        // m.obj = new SwitchMsg(buffer[i + 1], buffer[i + 2]);
        // mHandler.sendMessage(m);
        // }
        // i += 3;
        // break;

          default:
            Log.d(TAG, "unknown msg: " + buffer[i]);
            i = len;
            break;
        }
      }

    }
  }

  Handler mHandler = new Handler() {
    @Override
    public void handleMessage(Message msg) {
      switch (msg.what) {
      // case MESSAGE_SWITCH:
      // SwitchMsg o = (SwitchMsg) msg.obj;
      // handleSwitchMessage(o);
      // break;
      //
      // case MESSAGE_TEMPERATURE:
      // TemperatureMsg t = (TemperatureMsg) msg.obj;
      // handleTemperatureMessage(t);
      // break;
      //
      // case MESSAGE_LIGHT:
      // LightMsg l = (LightMsg) msg.obj;
      // handleLightMessage(l);
      // break;
      //
      // case MESSAGE_JOY:
      // JoyMsg j = (JoyMsg) msg.obj;
      // handleJoyMessage(j);
      // break;

      }
    }
  };

  public void sendCommand(byte command, byte[] buffer) {
    if (mOutputStream != null && buffer[1] != -1) {
      try {
        mOutputStream.write(buffer);
      } catch (IOException e) {
        Log.e(TAG, "write failed", e);
      }
    }
  }

  private final boolean SAFE_ANGLES = false;
  private final int MIN_SAFE_ANGLE = 45;
  private final int MAX_SAFE_ANGLE = 135;

  @Override
  public void moveServo(int servoIndex, int angle) {
    // TODO(bbrown): Assert if these are invalid casts.
    angle = makeSafeAngle(angle);
    byte buffer[] = {(byte) servoIndex, (byte) angle};
    try {
      mOutputStream.write('m');
      mOutputStream.write(buffer);
    } catch (IOException e) {
      e.printStackTrace();
    }
  }

  @Override
  public void moveAllServos(int[] angles) {
    // TODO(bbrown): Assert if these are invalid casts.
    int length = angles.length;
    byte buffer[] = new byte[length];
    for (int i = 0; i < length; ++i) {
      int currentAngle = makeSafeAngle(angles[i]);
      buffer[i] = (byte) (currentAngle);
    }
    try {
      mOutputStream.write('b');
      mOutputStream.write(buffer);
    } catch (IOException e) {
      e.printStackTrace();
    }
  }

  private int makeSafeAngle(int currentAngle) {
    if (SAFE_ANGLES) {
      currentAngle = currentAngle > MAX_SAFE_ANGLE ? MAX_SAFE_ANGLE : currentAngle;
      currentAngle = currentAngle < MIN_SAFE_ANGLE ? MIN_SAFE_ANGLE : currentAngle;
    }
    return currentAngle;
  }

  @Override
  public void attachServoToPin(int servoIndex, int pin) {
    // TODO(bbrown): Assert if these are invalid casts.
    byte buffer[] = {(byte) servoIndex, (byte) pin};
    try {
      mOutputStream.write('a');
      mOutputStream.write(buffer);
    } catch (IOException e) {
      e.printStackTrace();
    }
  }

  @Override
  public void detachServo(int servoIndex) {
    try {
      mOutputStream.write('d');
      mOutputStream.write(servoIndex);
    } catch (IOException e) {
      e.printStackTrace();
    }
  }

  @Override
  public IBinder onBind(Intent intent) {
    return mBinder;
  }

  private final IBinder mBinder = new LocalBinder();

  private String currentRobotConfig = SkipperPreferences.SKIPPER_LARGE;

  private int[] currentCalibrationShiftArray = null;

  public Robot getRobot() {
    return robot;
  }

  public void setRobotConfig(Context context, String robotConfig) {
    currentRobotConfig = robotConfig;
    currentCalibrationShiftArray = getSavedCalibrationShiftArray(
        context, PoseCreatorActivity.CALIBRATION_POSE_PREFIX + '_' + robotConfig);
    reloadRobot();
  }

  private void updateCalibration(int[] calibrationShiftArray) {
    currentCalibrationShiftArray  = calibrationShiftArray;
    reloadRobot();
  }

  private void reloadRobot() {
    if (SkipperPreferences.SKIPPER_LARGE.equals(currentRobotConfig)) {
      if (enabled) {
        robot = new SkipperRobot(this, new SkipperRobotConfig(currentCalibrationShiftArray));
      } else {
        robot = new ToastRobot(
            this, new DummyRobotController(), new SkipperRobotConfig(currentCalibrationShiftArray));
      }
    } else if (SkipperPreferences.SKIPPER_SMALL.equals(currentRobotConfig)) {
      if (enabled) {
        robot = new SkipperRobot(this, new SkipperJuniorRobotConfig(currentCalibrationShiftArray));
      } else {
        robot = new ToastRobot(this, new DummyRobotController(),
            new SkipperJuniorRobotConfig(currentCalibrationShiftArray));
      }
    } else {
      Log.d(TAG, "unknown robot config: " + currentRobotConfig );
    }
  }

  private int[] getSavedCalibrationShiftArray(Context context, String poseName) {
    int[] array = null;

    Pose pose = Pose.load(context, poseName);
    if (pose != null) {
      if (this != null) {
        Robot robot = getRobot();
        int numServos = robot.getNumServos();
        array = new int[numServos];

        for (int i = 0; i < numServos; ++i) {
          if(robot.getServo(i).getFlipped()) {
            array[i] = robot.getCalibrationAngle(i) - pose.getPosition(i);
          } else {
            array[i] = pose.getPosition(i) - robot.getCalibrationAngle(i);
          }
        }
      }
    } else {
      array = getResetCalibrationShiftArray();
    }

    return array;
  }

  private int[] getResetCalibrationShiftArray() {
    int[] array = null;
    if (this != null) {
      int numServos = getRobot().getNumServos();
      // Defaults to 0
      array = new int[numServos];
    }

    return array;
  }

  void resetCalibration() {
    updateCalibration(getResetCalibrationShiftArray());
  }

  void updateCalibrationFromSavedPose(Context context, String poseName) {
    updateCalibration(getSavedCalibrationShiftArray(context, poseName));
  }
}
